package org.qdog.pk.pkscrape.controller;

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.qdog.pk.pkscrape.entity.*;
import org.qdog.pk.pkscrape.util.RelationshipMap;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

import javax.annotation.Resource;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPathExpressionException;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;

/**
 * @since 08/2011
 */
@Component
public class EntityUpdater {
  private static final Logger log = Logger.getLogger(EntityUpdater.class);

  private DocumentBuilderFactory factory;
  private DocumentBuilder builder;
  private EntityMgr entityManager;
  private RelationshipMgr relationshipManager;

  public EntityUpdater() throws ParserConfigurationException {
    factory = DocumentBuilderFactory.newInstance();
    factory.setNamespaceAware(true);
    builder = factory.newDocumentBuilder();
  }

  @Transactional
  public void updateCity(City city, String xml)
        throws ParserConfigurationException, IOException, SAXException, XPathExpressionException {

    Document doc = builder.parse(new ByteArrayInputStream(xml.getBytes()));

    city = entityManager.createOrLoadEntity(city);
    RelationshipMap relationships = relationshipManager.loadRelationshipMap(city);

    // detect deleted entity
    String errorMessage = XPathHelper.xPathAsString(doc, "//*[@id='MainContent_CityBody_profileError']/b/font/text()");
    if (StringUtils.contains(errorMessage, "exist")) {
      deactivateEntity(city, relationships);
    } else {

      city.setName(XPathHelper.textByIdList(doc, "MainContent_CityBody_cityNameHeader"));

      StringBuilder location = new StringBuilder();
      location.append(XPathHelper.textByIdList(doc, "MainContent_CityBody_cityStateName"));
      if (StringUtils.isNotBlank(location.toString())) {
        location.append(", ");
      }
      location.append(XPathHelper.textByIdList(doc, "MainContent_CityBody_cityCountryName"));
      city.setLocation(location.toString());

      String foundedDateString = XPathHelper.xPathAsString(doc, "//*[@id='MainContent_CityBody_nonCityFoundedDate']/*/text()");
      if (StringUtils.isNotBlank(foundedDateString)) {
        try {
          city.setStart(new SimpleDateFormat("MM/dd/yyyy").parse(foundedDateString));
        } catch (ParseException e) {
          log.error("Invalid date format: " + foundedDateString);
        }
      }

      String totalResidents = XPathHelper.xPathAsString(doc, "//*[@id='MainContent_CityBody_nonTotalResidents']/*/text()");
      if (StringUtils.isNotBlank(totalResidents)) {
        city.setMemberCount(Integer.parseInt(totalResidents));
      }

      String activeResidents = XPathHelper.xPathAsString(doc, "//*[@id='MainContent_CityBody_nonActiveResidents']/*/text()");
      if (StringUtils.isNotBlank(activeResidents)) {
        city.setActiveMemberCount(Integer.parseInt(activeResidents));
      }

      String mayorUrl = XPathHelper.hrefByIdList(doc, "MainContent_CityBody_nonMayor", "MainContent_CityBody_mayor");
      relationshipManager.updateRelationships(CityMayor.class, extractEntityIdFromUrl(mayorUrl), relationships);

      Collection<String> founderUrls = XPathHelper.extractHyperlinks(doc, "//table[@id='MainContent_CityBody_nonCityfounderslist']");
      relationshipManager.updateRelationships(CityFounder.class, extractEntityIdsFromUrls(founderUrls), relationships);

      Collection<String> residentUrls = XPathHelper.extractHyperlinks(doc, "//div[@id='residents-section']");
      relationshipManager.updateRelationships(CityResident.class, extractEntityIdsFromUrls(residentUrls), relationships);

      Collection<String> enemyUrls = XPathHelper.extractHyperlinks(doc, "//div[@id='enemies-section']");
      relationshipManager.updateRelationships(Enemy.class, extractEntityIdsFromUrls(enemyUrls), relationships);

      city.setLastUpdate(new Date());

      // validate that the entity was extracted correctly
      if (StringUtils.isBlank(city.getName())) {
        throw new RuntimeException(city.getId() + " not parsed correctly");
      }
    }

  }

  @Transactional
  public void updatePerson(Person person, String xml)
        throws ParserConfigurationException, IOException, SAXException, XPathExpressionException {

    Document doc = builder.parse(new ByteArrayInputStream(xml.getBytes()));

    person = entityManager.createOrLoadEntity(person);
    RelationshipMap relationships = relationshipManager.loadRelationshipMap(person);

    // detect deleted entity
    String errorMessage = XPathHelper.xPathAsString(doc, "//*[@id='MainContent_ProfileBody_profileerror']/b/font/text()");
    if (StringUtils.contains(errorMessage, "deleted")) {
      deactivateEntity(person, relationships);
    } else {

      person.setName(XPathHelper.xPathAsString(doc, "//*[@id='MainContent_ProfileBody_character_name']/*/text()"));

      String cityUrl = XPathHelper.hrefByIdList(doc, "MainContent_ProfileBody_character_city");
      relationshipManager.updateRelationships(CityResident.class, extractEntityIdFromUrl(cityUrl), relationships);

      String kingdomUrl = XPathHelper.hrefByIdList(doc, "MainContent_ProfileBody_character_kingdom");
      relationshipManager.updateRelationships(KingdomMember.class, extractEntityIdFromUrl(kingdomUrl), relationships);

      String playingSinceString = XPathHelper.xPathAsString(doc, "//*[@id='MainContent_ProfileBody_character_playingsince']/*/text()");
      if (StringUtils.isNotBlank(playingSinceString)) {
        String parts[] = StringUtils.split(playingSinceString, " ");
        try {
          person.setStart(new SimpleDateFormat("MM/dd/yyyy").parse(parts[0]));
        } catch (ParseException e) {
          log.error("Invalid date format: " + parts[0]);
        }
      }

      Collection<String> friendUrls = XPathHelper.extractHyperlinks(doc, "//div[@id='friends-section']");
      relationshipManager.updateRelationships(Friend.class, extractEntityIdsFromUrls(friendUrls), relationships);

      Collection<String> enemyUrls = XPathHelper.extractHyperlinks(doc, "//div[@id='enemies-section']");
      relationshipManager.updateRelationships(Enemy.class, extractEntityIdsFromUrls(enemyUrls), relationships);

      person.setLastUpdate(new Date());

      // validate that the entity was extracted correctly
      if (StringUtils.isBlank(person.getName())) {
        throw new RuntimeException(person.getId() + " not parsed correctly");
      }
    }

  }

  private void deactivateEntity(Entity entity, RelationshipMap relationships) {
    log.info("Entity " + entity.getId() + " no longer exists");
    Date now = new Date();
    entity.setEnd(now);
    entity.setLastUpdate(now);
    relationships.deactivateAll(now);
  }

  @Transactional
  public void updateKingdom(Kingdom kingdom, String xml)
        throws ParserConfigurationException, IOException, SAXException, XPathExpressionException {
    Document doc = builder.parse(new ByteArrayInputStream(xml.getBytes()));

    kingdom = entityManager.createOrLoadEntity(kingdom);
    RelationshipMap relationships = relationshipManager.loadRelationshipMap(kingdom);

    // detect deleted entity
    String errorMessage = XPathHelper.xPathAsString(doc, "//*[@id='MainContent_KingdomBody_profileError']/b/font/text()");
    if (StringUtils.contains(errorMessage, "exist")) {
      deactivateEntity(kingdom, relationships);
    } else {
      kingdom.setName(XPathHelper.xPathAsString(doc, "//*[@id='MainContent_KingdomBody_kingdomNameHeader']/*/text()"));

      String foundedDateString = XPathHelper.xPathAsString(doc, "//*[@id='MainContent_KingdomBody_nonKingdomFoundedDate']/*/text()");
      if (StringUtils.isNotBlank(foundedDateString)) {
        try {
          kingdom.setStart(new SimpleDateFormat("MM/dd/yyyy").parse(foundedDateString));
        } catch (ParseException e) {
          log.error("Invalid date format: " + foundedDateString);
        }
      }

      String totalMembers = XPathHelper.xPathAsString(doc, "//*[@id='MainContent_KingdomBody_nonTotalMembers']/*/text()");
      if (StringUtils.isNotBlank(totalMembers)) {
        kingdom.setMemberCount(Integer.parseInt(totalMembers));
      }

      String activeMembers = XPathHelper.xPathAsString(doc, "//*[@id='MainContent_KingdomBody_nonActiveMembers']/*/text()");
      if (StringUtils.isNotBlank(activeMembers)) {
        kingdom.setActiveMemberCount(Integer.parseInt(activeMembers));
      }

      String influence = XPathHelper.xPathAsString(doc, "//*[@id='MainContent_KingdomBody_influence']/*/text()");
      if (StringUtils.isNotBlank(influence)) {
        kingdom.setInfluence(Integer.parseInt(influence));
      }

      String conquest = XPathHelper.xPathAsString(doc, "//*[@id='MainContent_KingdomBody_conquest']/*/text()");
      if (StringUtils.isNotBlank(conquest)) {
        kingdom.setConquest(Integer.parseInt(conquest));
      }

      String founderUrl = XPathHelper.hrefByIdList(doc, "MainContent_KingdomBody_nonFounder");
      relationshipManager.updateRelationships(KingdomFounder.class, extractEntityIdFromUrl(founderUrl), relationships);

      String rulerUrl = XPathHelper.hrefByIdList(doc, "MainContent_KingdomBody_nonRuler");
      relationshipManager.updateRelationships(KingdomRuler.class, extractEntityIdFromUrl(rulerUrl), relationships);

      Collection<String> residentUrls = XPathHelper.extractHyperlinks(doc, "//div[@id='members-section']");
      relationshipManager.updateRelationships(KingdomMember.class, extractEntityIdsFromUrls(residentUrls), relationships);

      Collection<String> enemyUrls = XPathHelper.extractHyperlinks(doc, "//div[@id='enemies-section']");
      relationshipManager.updateRelationships(Enemy.class, extractEntityIdsFromUrls(enemyUrls), relationships);

      kingdom.setLastUpdate(new Date());

      // validate that the entity was extracted correctly
      if (StringUtils.isBlank(kingdom.getName())) {
        throw new RuntimeException(kingdom.getId() + " not parsed correctly");
      }
    }

  }

  private Collection<Entity> extractEntitiesFromUrls(Collection<String> urls) {
    Collection<Entity> entities = new ArrayList<Entity>(urls.size());
    for (String url : urls) {
      entities.add(extractEntityFromUrl(url));
    }
    return entities;
  }

  private Collection<String> extractEntityIdsFromUrls(Collection<String> urls) {
    Collection<String> ids = new ArrayList<String>(urls.size());
    for (String url : urls) {
      ids.add(extractEntityIdFromUrl(url));
    }
    return ids;
  }

  private Entity extractEntityFromUrl(String url) {
    if (StringUtils.isBlank(url)) {
      return null;
    }
    String entityId = extractEntityIdFromUrl(url);
    EntityType entityType = EntityType.entityTypeFromId(entityId);
    return entityType.createEntity(entityId);
  }

  private String extractEntityIdFromUrl(String url) {
    String id = null;
    if (StringUtils.contains(url, "id=Chr")
          || StringUtils.contains(url, "id=Kng")
          || StringUtils.contains(url, "id=Cty")) {
      /*
       * Matching URLs:
       * Profile.aspx?charid=Chrxxxxxxxxxx
       * City.aspx?charid=Ctyxxxxxxxxxx
       * UrlHandler.aspx?id=Chrxxxxxxxxxx
       * UrlHandler.aspx?id=Kngxxxxxxxxxx
       * UrlHandler.aspx?id=Ctyxxxxxxxxxx
       */
      String urlParts[] = StringUtils.split(url, "=");
      id = urlParts[1];
    } else if (StringUtils.contains(url, "kingdomid=")) {
      /*
       * Matching URLs:
       * Kingdom.aspx?kingdomid=xxxxxxxxxxx
       */
      String urlParts[] = StringUtils.split(url, "=");
      id = "Kng" + urlParts[1];
    }
    return id;
  }

  @Resource(name = "entityMgr")
  public void setEntityManager(EntityMgr entityManager) {
    this.entityManager = entityManager;
  }

  @Resource(name = "relationshipMgr")
  public void setRelationshipManager(RelationshipMgr relationshipManager) {
    this.relationshipManager = relationshipManager;
  }
}
